package cn.iocoder.yudao.module.vos.service.marketingscore.admin;

import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.vos.controller.admin.marketingscore.vo.MarketingScorePageReqVO;
import cn.iocoder.yudao.module.vos.controller.admin.marketingscore.vo.MarketingScoreSaveReqVO;
import cn.iocoder.yudao.module.vos.dal.dataobject.ecdr.ECdrDO;
import cn.iocoder.yudao.module.vos.dal.dataobject.gatewaylevel.GatewayLevelDO;
import cn.iocoder.yudao.module.vos.dal.dataobject.marketingscore.MarketingScoreDO;
import cn.iocoder.yudao.module.vos.dal.mysql.ecdr.ECdrMapper;
import cn.iocoder.yudao.module.vos.dal.mysql.gatewaylevel.GatewayLevelMapper;
import cn.iocoder.yudao.module.vos.dal.mysql.marketingscore.MarketingScoreMapper;
import cn.iocoder.yudao.module.vos.job.ScoreTableJob;
import cn.iocoder.yudao.module.vos.job.vo.PhoneScoreDO;
import cn.iocoder.yudao.module.vos.utils.CdrUtil;
import com.mongodb.client.MongoCollection;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.vos.enums.ErrorCodeConstants.MARKETING_SCORE_NOT_EXISTS;

/**
 * 名单评分 Service 实现类
 *
 * 合并一次 Mongo 查询完成：评分 + 当天 tags + 网关等级（默认 1，命中则向上覆盖到 2/3）。
 *
 * @author shijun
 */
@Service
@Validated
@Slf4j
public class MarketingScoreServiceImpl implements MarketingScoreService {

    @Resource
    private MarketingScoreMapper marketingScoreMapper;
    @Resource
    private ECdrMapper eCdrMapper;
    @Resource
    private GatewayLevelMapper gatewayLevelMapper;
    @Resource
    private MongoTemplate mongoTemplate;

    @Value("${spring.datasource.dynamic.datasource.mysql2.name}")
    private String database2Name;

    // —— 可配置项（application.yml 可覆盖） —— //
    /** 打分统计窗口（包含 targetDate 在内向前 N 天） */
    @Value("${vos.marketing.score.window-days:14}")
    private int windowDays;
    /** 单号最多纳入多少条通话明细参与打分 */
    @Value("${vos.marketing.score.per-phone-limit:40}")
    private int perPhoneLimit;
    /** 批量落表大小 */
    @Value("${vos.marketing.score.bulk-size:1000}")
    private int bulkSize;
    /** “近期”命中天数（用于 recent 加分窗口） */
    @Value("${vos.marketing.score.recent-days:3}")
    private int recentDays;
    /** 深度沟通阈值（秒）：hold >= minEngagedSec 计 engaged */
    @Value("${vos.marketing.score.min-engaged-sec:60}")
    private int minEngagedSec;
    /** 普通接通阈值（秒）：minAnsweredSec <= hold < minEngagedSec 计 answered */
    @Value("${vos.marketing.score.min-answered-sec:15}")
    private int minAnsweredSec;
    /** 短通阈值（秒）：hold <= shortSec 计 short */
    @Value("${vos.marketing.score.short-sec:10}")
    private int shortSec;

    @Override
    public String createMarketingScore(MarketingScoreSaveReqVO createReqVO) {
        MarketingScoreDO marketingScore = BeanUtils.toBean(createReqVO, MarketingScoreDO.class);
        marketingScoreMapper.insert(marketingScore);
        return marketingScore.getPhoneE164();
    }

    @Override
    public void updateMarketingScore(MarketingScoreSaveReqVO updateReqVO) {
        validateMarketingScoreExists(updateReqVO.getPhoneE164());
        MarketingScoreDO updateObj = BeanUtils.toBean(updateReqVO, MarketingScoreDO.class);
        marketingScoreMapper.updateById(updateObj);
    }

    @Override
    public void deleteMarketingScore(String id) {
        validateMarketingScoreExists(id);
        marketingScoreMapper.deleteById(id);
    }

    @Override
    public void deleteMarketingScoreListByIds(List<String> ids) {
        marketingScoreMapper.deleteByIds(ids);
    }

    private void validateMarketingScoreExists(String id) {
        if (marketingScoreMapper.selectById(id) == null) {
            throw exception(MARKETING_SCORE_NOT_EXISTS);
        }
    }

    @Override
    public MarketingScoreDO getMarketingScore(String id) {
        return marketingScoreMapper.selectById(id);
    }

    @Override
    public PageResult<MarketingScoreDO> getMarketingScorePage(MarketingScorePageReqVO pageReqVO) {
        return marketingScoreMapper.selectPage(pageReqVO);
    }

    /**
     * 确保单日分表存在（不存在则创建，创建DDL内含 phone_md5 + 索引）
     */
    @Override
    public String ensureScoreTable(String day8) {
        String table = ScoreTableJob.scoreTable(day8);
        try {
            Integer exists = marketingScoreMapper.checkTableExists(table);
            if (exists != null && exists == 1) {
                return String.format("表 %s 已存在", table);
            }
        } catch (Exception e) {
            log.warn("checkTableExists 异常，继续尝试创建。 table={}, err={}", table, e.getMessage());
        }
        try {
            marketingScoreMapper.createScoreTable(table);
            return String.format("创建表 %s 成功（Mapper）", table);
        } catch (Exception e) {
            log.warn("Mapper 创建失败，fallback 到 JdbcTemplate。table={}, err={}", table, e.getMessage());
            return String.format("创建表 %s 失败（JdbcTemplate）", table);
        }
    }

    @Override
    public void buildDayScores(String day, int cooldownDays) {
        String day8 = ScoreTableJob.toDay8(day);
        // === 新增：转成 yyyy-MM-dd 作为 list_date ===
        String listDate = CdrUtil.toYmd(day8);

        String cdrTable = "e_cdr_" + day8;
        Integer exists = eCdrMapper.checkTableExists(database2Name, cdrTable);
        if (exists == null || exists != 1) {
            throw new IllegalStateException("CDR 日表不存在: " + cdrTable);
        }
        ensureScoreTable(day8);
        String scoreTable = ScoreTableJob.scoreTable(day8);

        Long cnt = marketingScoreMapper.countAll(scoreTable);
        if (cnt != null && cnt > 0) {
            log.info("[score] {} already built, skip.", scoreTable);
            return;
        }

        // 当天唯一号码
        List<String> phones = eCdrMapper.listPhonesByDay(cdrTable);
        log.info("[score] {} unique phones={}", day8, phones.size());
        if (phones.isEmpty()) return;

        // 预计算窗口 & 前N天
        String[] scoreDays = buildBackwardDays(day8, windowDays);
        String[] prevN = buildBackwardDays(day8, cooldownDays + 1);
        prevN = Arrays.copyOf(prevN, prevN.length - 1); // 排除当天，仅前N天

        // —— 只查一次网关等级映射 —— //
        List<GatewayLevelDO> gatewayLevels = gatewayLevelMapper.selectList();
        List<LevelPattern> levelPatterns = buildLevelPatterns(gatewayLevels);

        // 计算分数 & 批量落表（前N天出现过的号码跳过）
        List<PhoneScoreDO> batch = new ArrayList<>(bulkSize);
        for (String phone : phones) {
            phone = CdrUtil.trimNumber(phone);
            if (seenInPreviousDays(phone, prevN)) continue;

            AggResult ar = computeScoreTagsAndLevelFromMongo(
                    phone, scoreDays, perPhoneLimit, day8, levelPatterns);

            PhoneScoreDO row = new PhoneScoreDO(
                    phone,
                    BigDecimal.valueOf(ar.score),
                    CdrUtil.md5(phone),
                    ar.tags,
                    String.valueOf(ar.level),
                    listDate // <<< 新增：名单日期 yyyy-MM-dd
            );

            batch.add(row);
            if (batch.size() >= bulkSize) {
                marketingScoreMapper.insertScoresToTable(scoreTable, batch);
                batch.clear();
            }
        }
        if (!batch.isEmpty()) {
            marketingScoreMapper.insertScoresToTable(scoreTable, batch);
            batch.clear();
        }
        log.info("[score] {} finished.", day8);
    }

    /**
     * 重建（可选）：清空后重算
     */
    @Override
    public void rebuildDayScores(String day, int cooldownDays) {
        String day8 = ScoreTableJob.toDay8(day);
        ensureScoreTable(day8);
        String table = ScoreTableJob.scoreTable(day8);
        marketingScoreMapper.truncateTable(table);
        buildDayScores(day8, cooldownDays);
    }

    // ======= Private Helpers =======

    private static String[] buildBackwardDays(String day8, int n) {
        LocalDate d = LocalDate.of(
                Integer.parseInt(day8.substring(0, 4)),
                Integer.parseInt(day8.substring(4, 6)),
                Integer.parseInt(day8.substring(6, 8)));
        String[] arr = new String[n];
        for (int i = n - 1; i >= 0; i--) {
            LocalDate x = d.minusDays(i);
            arr[n - 1 - i] = String.format("%04d%02d%02d", x.getYear(), x.getMonthValue(), x.getDayOfMonth());
        }
        return arr;
    }

    private boolean seenInPreviousDays(String phone, String[] prevDays) {
        MongoCollection<Document> cdr = mongoTemplate.getDb().getCollection("cdr");
        Document doc = cdr.find(new Document("_id", phone))
                .projection(new Document("records", 1))
                .first();
        if (doc == null) return false;
        Document records = (Document) doc.get("records");
        if (records == null) return false;
        for (String d : prevDays) {
            Object v = records.get(d);
            if (v instanceof List && !((List<?>) v).isEmpty()) return true;
        }
        return false;
    }

    private static long eodMs(String yyyymmdd) {
        LocalDate d = LocalDate.of(
                Integer.parseInt(yyyymmdd.substring(0, 4)),
                Integer.parseInt(yyyymmdd.substring(4, 6)),
                Integer.parseInt(yyyymmdd.substring(8 - 2)));
        return d.plusDays(1).atStartOfDay(CdrUtil.ZONE).toInstant().toEpochMilli() - 1;
    }

    /** 等级短名匹配器 */
    private static class LevelPattern {
        final String shortNameLower; // gatewayShortName 小写
        final int level;             // 1/2/3
        LevelPattern(String shortNameLower, int level) {
            this.shortNameLower = shortNameLower;
            this.level = level;
        }
    }

    private static List<LevelPattern> buildLevelPatterns(List<GatewayLevelDO> list) {
        if (list == null || list.isEmpty()) return Collections.emptyList();
        List<LevelPattern> out = new ArrayList<>(list.size());
        for (GatewayLevelDO g : list) {
            String sn = g.getGatewayShortName();
            if (StrUtil.isBlank(sn)) continue;
            int lv = parseLevel(g.getListLevel()); // 支持 "1"/"2"/"3"
            if (lv <= 0) continue;
            out.add(new LevelPattern(sn.toLowerCase(Locale.ROOT), lv));
        }
        return out;
    }

    private static int parseLevel(String s) {
        if (StrUtil.isBlank(s)) return 0;
        try {
            int v = Integer.parseInt(s.trim());
            if (v < 1 || v > 3) return 0;
            return v;
        } catch (Exception ignore) {
            return 0;
        }
    }

    private static Long asLong(Object v) {
        return v == null ? null :
                (v instanceof Number ? ((Number) v).longValue() : Long.parseLong(v.toString()));
    }

    private static Integer asInt(Object v) {
        return v == null ? null :
                (v instanceof Number ? ((Number) v).intValue() : Integer.parseInt(v.toString()));
    }

    /** 聚合结果：一次 Mongo 读出的评分/标签/等级 */
    private static class AggResult {
        final double score;
        final String tags; // 目标日 day8 的 tags（逗号分隔）
        final int level;   // 1/2/3（默认至少为 1）
        AggResult(double score, String tags, int level) {
            this.score = score;
            this.tags = tags;
            this.level = level;
        }
    }

    /**
     * 一次读取并完成：
     * 1) 窗口内评分（engaged/answered/short/recent/avgHold）
     * 2) 目标日 day8 的 tags 计算（基于 CdrUtil.computePhoneTag）
     * 3) 目标日 day8 的网关等级归并（默认 1；同日多通话取最大等级 1<2<3）
     */
    @SuppressWarnings("unchecked")
    private AggResult computeScoreTagsAndLevelFromMongo(String phone,
                                                        String[] days,
                                                        int limitPerPhone,
                                                        String targetDay8,
                                                        List<LevelPattern> levelPatterns) {
        MongoCollection<Document> cdr = mongoTemplate.getDb().getCollection("cdr");
        Document doc = cdr.find(new Document("_id", phone)).first();
        if (doc == null) {
            // 没有 cdr 文档也给默认等级 1
            return new AggResult(-999D, "", 1);
        }

        // 评分阈值准备
        int rd = Math.max(1, recentDays);
        long recentThresholdMs = eodMs(days[days.length - 1]) - (rd - 1) * 24L * 3600_000;
        int MIN_ENGAGED = Math.max(minEngagedSec, minAnsweredSec + 1);
        int MIN_ANSWERED = Math.min(minAnsweredSec, MIN_ENGAGED - 1);
        int SHORT_SEC = Math.max(0, shortSec);

        int engaged = 0, answered = 0, shortCnt = 0, cnt = 0;
        long holdSum = 0L;
        boolean recent = false;

        // tags 去重保序
        Set<String> dayTags = new LinkedHashSet<>();
        int bestLevel = 1; // —— 默认等级 1 —— //

        Document records = (Document) doc.getOrDefault("records", new Document());

        outer:
        for (int i = days.length - 1; i >= 0; i--) {
            String dKey = days[i];
            List<Document> calls = (List<Document>) records.get(dKey);
            if (calls == null || calls.isEmpty()) continue;

            // 评分：按倒序（近期优先）扫描
            for (int k = calls.size() - 1; k >= 0; k--) {
                Document c = calls.get(k);

                Integer holdObj = asInt(c.get("holdtime"));
                Long startObj = asLong(c.get("starttime"));
                int hold = holdObj == null ? 0 : holdObj;
                long st = startObj == null ? 0L : startObj;

                // 评分统计
                holdSum += hold;
                cnt++;
                if (hold >= MIN_ENGAGED) {
                    engaged++;
                } else if (hold >= MIN_ANSWERED) {
                    answered++;
                }
                if (hold <= SHORT_SEC) {
                    shortCnt++;
                }
                if (st >= recentThresholdMs && hold >= MIN_ANSWERED) {
                    recent = true;
                }

                // 仅在目标日计算 tags 和 等级
                if (StrUtil.equals(dKey, targetDay8)) {
                    // 1) tags
                    ECdrDO row = new ECdrDO();
                    row.setCallergatewayid((String) c.get("callergatewayid"));
                    row.setCalleeaccesse164((String) c.get("calleeaccesse164"));
                    String calleeToGw = (String) c.get("calleetogatewaye164");
                    if (calleeToGw == null) calleeToGw = phone;
                    row.setCalleetogatewaye164(calleeToGw);
                    String tag = CdrUtil.computePhoneTag(row); // "1"/"2"/"3"
                    if (StrUtil.isNotBlank(tag)) {
                        dayTags.add(tag);
                    }

                    // 2) 等级（忽略大小写匹配；同日多通话取最大等级；未命中保持默认 1）
                    if (levelPatterns != null && !levelPatterns.isEmpty()) {
                        String cg = (String) c.get("callergatewayid");
                        if (StrUtil.isNotBlank(cg)) {
                            String cgNorm = cg.toLowerCase(Locale.ROOT);
                            for (LevelPattern lp : levelPatterns) {
                                // 这里对模式也统一小写，确保忽略大小写
                                String pat = lp.shortNameLower; // 若你的 LevelPattern 没有该字段，可换成 lp.shortName
                                if (StrUtil.isBlank(pat)) continue;
                                String patNorm = pat.toLowerCase(Locale.ROOT);
                                if (cgNorm.contains(patNorm) && lp.level > bestLevel) {
                                    bestLevel = lp.level;
                                }
                            }
                        }
                    }
                }

                if (cnt >= limitPerPhone) break outer;
            }
        }

        double avgHold = cnt == 0 ? 0.0 : (holdSum * 1.0 / cnt);
        double score = (3.0 * engaged) + (1.0 * answered) - (2.0 * shortCnt)
                + (recent ? 2.0 : 0.0) + (avgHold / 60.0);

        String tagsJoined = dayTags.isEmpty() ? "" : String.join(",", dayTags);
        return new AggResult(score, tagsJoined, bestLevel);
    }

}